package com.example.algorithm.dynamicprogramming;

import java.util.Collections;
import java.util.PriorityQueue;
import java.util.Queue;

/**
 * 汽车从起点出发驶向目的地，该目的地位于出发位置东面 target 英里处。
 *  沿途有加油站，每个 station[i] 代表一个加油站，
 *  它位于出发位置东面 station[i][0] 英里处，并且有 station[i][1] 升汽油。
 *
 *  假设汽车油箱的容量是无限的，其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。
 *  当汽车到达加油站时，它可能停下来加油，将 所有汽油 从加油站转移到汽车中。
 *  为了到达目的地，汽车所必要的最低加油次数是多少？如果无法到达目的地，则返回 -1 。
 *  注意：如果汽车到达加油站时剩余燃料为 0，它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0，仍然认为它已经到达目的地。
 *
 *  示例 1：
 *  输入：target = 1, startFuel = 1, stations = []
 * 输出：0
 * 解释：我们可以在不加油的情况下到达目的地。
 *
 *  示例 2：
 *  输入：target = 100, startFuel = 1, stations = [[10,100]]
 * 输出：-1
 * 解释：我们无法抵达目的地，甚至无法到达第一个加油站。
 *
 *  示例 3：
 *  输入：target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
 * 输出：2
 * 解释：
 * 我们出发时有 10 升燃料。
 * 我们开车来到距起点 10 英里处的加油站，消耗 10 升燃料。将汽油从 0 升加到 60 升。
 * 然后，我们从 10 英里处的加油站开到 60 英里处的加油站（消耗 50 升燃料），
 * 并将汽油从 10 升加到 50 升。然后我们开车抵达目的地。
 * 我们沿途在1两个加油站停靠，所以返回 2 。
 */
public class Leetcode871_MinRefuelStops {
    public static void main(String[] args) {
        int target = 100;
        int startFuel = 10;
        int[][] stations = {{10,60},{20,30},{30,30},{60,40}};
        System.out.println(new Solution().minRefuelStops(target, startFuel, stations));



    }


    static class Solution {
        /**
         * 解法二:堆
         * 在第i个加油站最大加油次数为i次
         * 在动态规划解法中每次增加加油站的个数，需要从后往前更新dp数组,有大量的重复工作
         * 如果可以记录每个加油站的油量，则不需要从后往前遍历
         *
         * 通过分析发现:
         *   可以到达的最远距离取决于加油站油量的分布，而不是位置的分布
         *   i+1步可以到达的最大距离= i步可以到达的最大距离+ 汽车目前可以经过的最大加油站的油量
         *
         * @param target
         * @param tank 油箱里面剩余的油量
         * @param stations
         * @return
         */
        public int minRefuelStops2(int target, int tank, int[][] stations) {
            // 最大堆用于保存驶过的加油站的油量
            Queue<Integer> fuelMaxHeap = new PriorityQueue<>(Collections.reverseOrder());
            int res = 0, prev = 0;// prev表示 上一个加油站到出发点的距离
            for (int[] station : stations) {
                int location = station[0];
                int capacity = station[1];
                // 到一个加油站后更新油箱里的油
                tank = tank - (location - prev);
                // tank < 0表示在之前如果不加油则不可能能到底当前的加油站
                while (tank < 0 && !fuelMaxHeap.isEmpty()) { // 挑选出到目前所有经过的加油站的最大的加油站进行加油
                    tank += fuelMaxHeap.poll();
                    res++;
                }

                if (tank < 0) { // 经过的所有加油站都进行加油还是不能到达当前加油站,说明不可能完成任务
                    return -1;
                }
                fuelMaxHeap.offer(capacity);// 记录当前加油站的油量
                prev = location;// 记录当前加油站与出发点的距离
            }
            // 假设target位置也有⼀一个加油站，油量量为0
            tank -= target - prev;
            while (!fuelMaxHeap.isEmpty() && tank < 0) {
                tank += fuelMaxHeap.poll();
                res++;
            }
            if (tank < 0) return -1;
            return res;
        }

        /**
         * 解法一:动态规划
         *
         * 动态规划性质:
         *   1.最优子结构
         *     如果问题的最优解所包含的子问题的解也是最优的，我们就称该问题具有最优子结构性质
         *   2.重复子问题
         *     求解原问题时，每次产生的子问题并不总是新问题，有些子问题会被重复计算多次。
         *     动态规划算法利用了重复子问题的性质，对每一个子问题只计算一次，将结果加入备忘录
         *     当再次需要计算已经计算过的子问题时，只需查找备忘录，从而获得较高的效率。(空间换时间)
         *   3.无后效性
         *     子问题的解一旦确定，就不再改变，不受在这之后、包含它的更大的问题的求解决策影响。
         *
         * 动态规划求解方向:
         *   1. 自底向上（递推）
         *   2. 自顶向下（递归）
         *
         * 解题思路:
         *  1.原问题与子问题:
         *    在所有加油站加油i次能行驶的最大距离
         *  2.设计状态:
         *    dp[i]表示加油i 次可以行驶的最远距离(每增加一个加油站就得更新所有dp值)
         *  3.状态转移方程:
         *    每多一个加油站stations[i]={location,capacity},
         *    如果之前通过加t次油能到第i个加油站的位置,那么通过在下一个(i+1)加油站加油可以行驶的最远距离
         *    dp[t+1]=max(dp[t+1],dp[t]+capacity)
         *
         *
         *  4.边界值
         *    dp[0]=startFuel
         * @param target
         * @param startFuel
         * @param stations
         * @return
         */
        public int minRefuelStops1(int target, int startFuel, int[][] stations) {
            long[] dp = new long[stations.length + 1]; // 加油i 次可以行驶的最远距离
            dp[0] = startFuel;

            for (int i = 0; i < stations.length; i++) { // 有多少可加油站可以选择(每次增加一个)
                for (int t = i; t >= 0; --t) {
                    if (dp[t] >= stations[i][0]) { // 加油t次可以到达第i个加油站
                        // 每多一个新的加油站，就需要更新所有加油次数的值
                        dp[t + 1] = Math.max(dp[t + 1], dp[t] + (long) stations[i][1]);
                    }
                }
            }
            // 找出可以到达目的地的最小加油次数
            for (int i = 0; i < dp.length; i++) {
                if (dp[i] >= target) return i;
            }
            return -1;
        }

        public int minRefuelStops(int target, int startFuel, int[][] stations) {
            return minRefuelStops1(target, startFuel, stations);
        }
    }
}
